package com.atguigu.tingshu.common.login.aspect;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.login.annotation.TingshuLogin;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Author:huzhongkui
 * Date: 2025-03-11 星期二 14:49:12
 * Description:
 */
@Aspect
@Component
@Slf4j
public class LoginAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;


    /**
     * 定义切面逻辑(判断请求的资源是否登录  如果登录 直接访问这个资源 反之去登录)
     * <p>
     * 切入点表达式 有9种
     * <p>
     * 通知类型：5种
     * 前置通知：
     * 特点：切面逻辑在执行目标方法之前执行到。
     * 用法：@Before
     * <p>
     * <p>
     * 返回通知（肯定获取不到执行目标方法期间出现异常对象 但是可以拿到执行完目标方法的结果）
     * 特点: 切面逻辑在执行完目标方法之后（执行目标方法期间不会出现异常） 来执行到
     * 用法：@AfterReturning
     * <p>
     * <p>
     * 后置通知：（不能捕获到执行目标方法期间出现的异常对象）
     * 特点: 切面逻辑在执行完目标方法之后（执行目标方法期间不管是否出现异常） 来执行到
     * 用法：@After
     * <p>
     * <p>
     * 异常通知：（可以捕获到了执行目标方法期间出现的异常对象 肯定获取不到执行目标方法后的结果对象）
     * 特点：切面逻辑在执行完目标方法之后（执行目标方法期间不管出现异常） 来执行到
     * 用法：@AfterThrowing
     * <p>
     * <p>
     * 环绕通知：（ProceedingJoinPoint 即能获取到执行目标方法后的结果对象 也能获取到执行目标方法期间的异常对象）
     * 特点：目标方法执行前后以及执行目标方法期间是否出现异常都会执行切面逻辑
     * 用法：@Around
     * <p>
     * <p>
     * <p>
     * <p>
     * ThreadLocal在使用的过程中注意内存泄漏的问题
     * <p>
     * 内存泄漏：一个线程将对象（数据）存放到了内存的堆空间，但是当这个线程干完活之后 没有将堆中刚刚放进去的对象（数据）清空。
     * 一次内存泄漏可以不用管 但是久而久之 内存泄漏就会导致OOM.
     * <p>
     * 使用ThreadLocal也不一定出现内存泄漏
     * 1.new Thread(()->{
     * <p>
     * ThreadLocal threadLocal=  new  ThreadLocal()
     * threadLocal.set("1");
     * <p>
     * //  a()
     * //  b()
     * <p>
     * })
     * <p>
     * 2.   ExecutorService executorService = Executors.newFixedThreadPool(1);
     * executorService.execute(new Runnable() {
     *
     * @Override public void run() {
     * ThreadLocal threadLocal=  new  ThreadLocal();
     * threadLocal.set("1");
     * a()
     * b()
     * threadLocal.remove（）
     * }
     * })
     * Tomcat线程池：一上来池中就有默认的10个线程 最大默认的线程数200
     */

//认证模块
    //  多切面的失效问题
    //  1.登录切面(业务)
    //  2.事务切面（非业务）
    @Around(value = "@annotation(com.atguigu.tingshu.common.login.annotation.TingshuLogin)")
    public Object loginCheck(ProceedingJoinPoint pjp) throws Throwable {
       //token ==jsonWebToken
//jsonWebToken就是前端穿过来的token值
        // 1.获取请求中的令牌
        String jsonWebToken = getJsonWebToken();

        // 2.获取目标方法的注解以及属性值
        Boolean isLogin = getMethodLoginAnnotationValue(pjp);
        if (!isLogin) {   // 不一定要登录
            // 如果你要登录了也即带了token  那么也要把用户id传过来
            if (StringUtils.isEmpty(jsonWebToken)) {
                return pjp.proceed();
            }
        }

        // 3. 判断是否携带了令牌（如果携带令牌还将载荷中的数据获取到）
        Long userId = checkTokenAndGetUserId(jsonWebToken);

        // 4. 将认证中心获取到的userId放到ThreadLocal中
        AuthContextHolder.setUserId(userId);
//        ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
//        longThreadLocal.set(userId);

        Object retVal;  //  执行目标方法
        try {
            retVal = pjp.proceed();
        } finally {
            AuthContextHolder.removeUserId();  // 解决内存泄漏
        }

        // 5. 返回结果
        return retVal;
    }

    private Boolean getMethodLoginAnnotationValue(ProceedingJoinPoint pjp) {

        // 1.获取方法签名对象
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();

        // 2.获取方法对象
        Method method = methodSignature.getMethod();

        // 3.获取方法上的TingshuLogin注解对象
        TingshuLogin annotation = method.getAnnotation(TingshuLogin.class);

        // 4.获取注解对象的属性值
        boolean required = annotation.required();

        // 5.返回
        return required;

    }

    private Long checkTokenAndGetUserId(String jsonWebTokenFromWeb) {

        // 1.校验是否该请求中携带了
        if (StringUtils.isEmpty(jsonWebTokenFromWeb)) {
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
        }

        // 2.校验jsonWebToken是否被篡改了。公钥解密并验证看是否相等
        Jwt jwt = JwtHelper.decodeAndVerify(jsonWebTokenFromWeb, new RsaVerifier(PublicConstant.PUBLIC_KEY));

        // 3.校验通过 获取载荷数据
        String claims = jwt.getClaims();
        Map map = JSONObject.parseObject(claims, Map.class);
        Object userId = map.get("id");
        Object openId = map.get("openId");


        // 4.比对Redis中是否存在jsonWebToken
//        利用token载荷信息获取redis中缓存的key值
        String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
        String accessTokenFromRedis = redisTemplate.opsForValue().get(accessTokenKey);
//        判断redis中缓存的key值是否为空，以及是否与前端传来的token值相等
        if (StringUtils.isEmpty(accessTokenFromRedis) || !jsonWebTokenFromWeb.equals(accessTokenFromRedis)) {
            throw new GuiguException(401, "accessToken已过期");
        }


        return Long.parseLong(userId.toString());


    }

    private static String getJsonWebToken() {
        // 1.获取用户的身份信息
        // 1.1获取目标请求属性对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 1.2 获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();
        // 1.3 获取请求对象的请求头
        String token = request.getHeader("token");
        return token;
    }


}
